feat(server): server-side response cache for proxied actuator endpoints#5266
Open
mmorel-35 wants to merge 1 commit intocodecentric:masterfrom
Open
feat(server): server-side response cache for proxied actuator endpoints#5266mmorel-35 wants to merge 1 commit intocodecentric:masterfrom
mmorel-35 wants to merge 1 commit intocodecentric:masterfrom
Conversation
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Expensive, mostly-static actuator endpoints (mappings, beans, configprops, etc.) are re-fetched from every monitored app on every SBA UI request. In clustered deployments each SBA node independently re-fetches the same data. This adds a server-side response cache for proxied actuator GET requests, with an optional Hazelcast-backed shared store for multi-node deployments.
Cache core (
web/cache/)CacheEntry— immutable serializable snapshot of a proxied response (status code, defensively-copied headers, body bytes, timestamp); sensitive headers are stripped inInstanceWebProxybefore the entry is created. Exposes a zero-copygetBodyRef()for internal use so cache hits avoid a per-hit defensive clone.ActuatorResponseCache— interface:get / put / invalidateAllForInstance / invalidateEndpointForInstance / shouldCache / getMaxPayloadSizeCacheKeyBuilder— hashes theinstanceIdwith SHA-256 before concatenating the endpoint path, producing an unambiguous key that is safe even wheninstanceIdvalues contain:(e.g. CloudFoundryapplicationId:instanceIdformat). BothInMemoryActuatorResponseCacheandHazelcastActuatorResponseCachedelegate all key construction and prefix matching to this helper.InMemoryActuatorResponseCache—ConcurrentHashMapwith lazy TTL eviction (default, single-node)HazelcastActuatorResponseCache—IMapwith native per-entry TTL; invalidation uses server-sidePredicates+IMap.removeAllto avoid full in-JVM key scans; shared across all cluster nodes when Hazelcast is presentCacheInvalidationTrigger— event-driven invalidation onInstanceDeregisteredEvent,InstanceRegistrationUpdatedEvent,InstanceEndpointsDetectedEventConfiguration (
AdminServerProperties.EndpointCacheProperties)New
spring.boot.admin.endpoint-cache.*properties:Also adds
spring.boot.admin.hazelcast.response-cacheto name the HazelcastIMap(default:spring-boot-admin-actuator-response-cache), consistent with the existingevent-storeandsent-notificationsHazelcast properties.Proxy integration (
InstanceWebProxy)Cache logic lives entirely inside
InstanceWebProxyas an optional internal collaborator. When anActuatorResponseCache(and companionHttpHeaderFilter) is supplied via the second constructorInstanceWebProxy(InstanceWebClient, ActuatorResponseCache, HttpHeaderFilter)(both must be non-null; constructor enforces this), the proxy transparently:max-payload-sizePOST/PUT/PATCH/DELETEto a configured endpoint path, evicts that endpoint's cache entries so the next GET returns fresh dataFlux<Instance>) — always forwarded upstream, never cachedAll blocking cache operations (
get,put,invalidateEndpointForInstance) are offloaded toSchedulers.boundedElastic()so they never stall Netty event-loop threads. Cache read failures are treated as a cache miss (logged as a warning with full stack trace, request continues upstream) so transient Hazelcast network/serialization errors never fail a proxied request. Cache write failures (put,invalidate) are also handled with.onErrorResume(...)— a warning is logged but the response still reaches the client.Only responses with a known
Content-Lengththat fits withinmax-payload-sizeare cached. Responses whereContent-Lengthis absent (chunked/streamed) are forwarded as-is without buffering, avoiding any unbounded memory allocation. Responses with a knownContent-Lengthexceedingmax-payload-sizeare also forwarded directly.The
InstanceIdused as the cache key is derived from the already-resolvedInstanceinternally — no change to the publicforward(Mono<Instance>, ForwardRequest, Function)API. Non-GET methods on unconfigured endpoint ids, and error responses bypass the cache entirely.Wiring
AdminServerWebConfigurationconstructsHttpHeaderFilterandInstanceWebProxy(with the optional cache collaborator) and injects them into both proxy controllers. The controller constructors are(String adminContextPath, HttpHeaderFilter, InstanceRegistry, InstanceWebProxy)— they receive all collaborators and build nothing themselves.Both
reactive/InstancesProxyControllerandservlet/InstancesProxyControllerare pure routing/response-writing layers with no cache logic and no knowledge of howInstanceWebProxyis instantiated.AdminServerWebConfiguration— registersInMemoryActuatorResponseCache(@ConditionalOnMissingBean+@ConditionalOnProperty) andCacheInvalidationTriggerbeans; buildsHttpHeaderFilter+InstanceWebProxyand injects them into both proxy controllersAdminServerHazelcastAutoConfiguration— registersHazelcastActuatorResponseCache(@ConditionalOnMissingBean+@ConditionalOnProperty) when aHazelcastInstanceis present, taking precedence over the in-memory defaultCache beans are not created at all when
spring.boot.admin.endpoint-cache.enabled=false, following standard Spring Boot@ConditionalOnPropertypatterns.Tests
InMemoryActuatorResponseCacheTest— key isolation, TTL expiry (using Awaitility, noThread.sleep), per-endpoint TTL overrides, instance invalidation, single-endpoint invalidation (exact match, sub-path, and query-string variants), disabled mode,shouldCacheguards (13 tests); allgetBytes()/new String(bytes)calls use explicitStandardCharsets.UTF_8HazelcastActuatorResponseCacheTest— embedded Hazelcast validates key matching (exact, sub-path, query-string variants),invalidateAllForInstance,invalidateEndpointForInstance,shouldCacheguards, and per-endpoint TTL behaviorCacheInvalidationTriggerTest— event-driven invalidation; non-invalidating events leave cache intact; usesTestPublisherwith subscription-await to eliminate FAIL_ZERO_SUBSCRIBER race;@AfterEachstops the trigger to prevent thread/subscription leaks across tests (4 tests)AbstractInstancesProxyControllerIntegrationTest— two new tests covering cache hit (WireMock receives exactly 1 upstream call across 2 proxy requests) and non-default endpoint bypass; WireMockContent-Lengthstub header computed from UTF-8 bytes; run in both reactive and servlet variants